package org.example.mymq.mqserver.datacenter;

import org.example.mymq.common.BinaryTool;
import org.example.mymq.common.MqException;
import org.example.mymq.mqserver.core.MSGQueue;
import org.example.mymq.mqserver.core.Message;

import java.io.*;
import java.util.LinkedList;
import java.util.Scanner;

public class MessageFileManager {
    static public class Stat{
//        消息总数
        public int totalCount;
//        消息有效数目
        public int validCount;
    }

    private String getQueueDir(String queueName){
        return "/data/"+queueName;
    }
    private String getQueueDataPath(String queueName){
        return getQueueDir(queueName)+"/queue_data.txt";
    }
    private String getQueueStatPath(String queueName){
        return getQueueDir(queueName)+"/queue_stat.txt";
    }

//    读统计文件
    private Stat readStat(String queueName){
        Stat stat = new Stat();
        try(InputStream inputStream = new FileInputStream(getQueueStatPath(queueName))){
            Scanner scanner = new Scanner(inputStream);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
            return stat;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
//    写统计文件
    private void writeStat(Stat stat ,String queueName){
        try(OutputStream outputStream = new FileOutputStream(getQueueStatPath(queueName))) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.write(stat.totalCount+"\t"+stat.validCount);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void createQueueFiles(String queueName) throws IOException {
//        1.创建文件目录
        File baseDir = new File(getQueueDir(queueName));
        if (!baseDir.exists()){
            boolean ok = baseDir.mkdirs();
            if (!ok){
                throw new IOException("创建目录失败！baseDir="+baseDir.getAbsolutePath());
            }
        }
//        2.创建队列数据文件		   queueDataFile
        File queueDataFile = new File(getQueueDataPath(queueName));
        if (!queueDataFile.exists()){
            boolean ok = queueDataFile.createNewFile();
            if (!ok){
                throw new IOException("创建队列数据文件失败 queueDataFile="+queueDataFile.getAbsolutePath());
            }
        }
//        创建消息统计文件		   queueStatFile
        File queueStatFile = new File(getQueueStatPath(queueName));
        if (!queueStatFile.exists()){
            boolean ok = queueStatFile.createNewFile();
            if (!ok){
                throw new IOException("创建数据统计文件失败 queueStatFile="+queueStatFile.getAbsolutePath());
            }
        }

//        给统计文件设定初始值.    0\t0
        Stat stat = new Stat();
        stat.totalCount = 0;
        stat.validCount = 0;
        writeStat(stat,queueName);

    }

    public void destroyQueueFiles(String queueName) throws IOException {
        File queueDataFile = new File(getQueueDataPath(queueName));
        boolean ok1 = queueDataFile.delete();
        File queueStatFile = new File(getQueueStatPath(queueName));
        boolean ok2 = queueStatFile.delete();
        File baseDir = new File(getQueueDir(queueName));
        boolean ok3 = baseDir.delete();
        if (!ok1 || !ok2 || !ok3){
            throw new IOException("删除队列目录和文件失败，baseDir："+getQueueDir(queueName));
        }
    }

    public boolean checkFilesExists(String queueName){
        File queueDataFile = new File(getQueueDataPath(queueName));
        if (!queueDataFile.exists()){
            return false;
        }
        File queueStatFile = new File(getQueueStatPath(queueName));
        if (!queueStatFile.exists()){
            return false;
        }
        return true;
    }

//    写入消息
    public void sendMessage(MSGQueue queue, Message message) throws IOException, MqException {
//        1.检查当前队列的对应文件是否存在
        boolean ok = checkFilesExists(queue.getName());
        if (!ok){
            throw new MqException("[MessageFileManager]队列对应的文件不存在 queueName="+queue.getName());
        }
//        2.
        byte[] messageBinary = BinaryTool.toBytes(message);
        synchronized (queue){
            File queueDataFile = new File(getQueueDataPath(queue.getName()));
            message.setOffsetBeg(queueDataFile.length()+4);
            message.setOffsetEnd(queueDataFile.length()+4+messageBinary.length);
            try(OutputStream outputStream = new FileOutputStream(getQueueDataPath(queue.getName()),true)){
                try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
                    dataOutputStream.write(messageBinary.length);
                    dataOutputStream.write(messageBinary);
                }
            }
//        3.
            Stat stat = readStat(queue.getName());
            stat.totalCount+=1;
            stat.validCount+=1;
            writeStat(stat,queue.getName());
        }
    }

//    删除
    public void deleteMessage(MSGQueue queue,Message message) throws IOException, ClassNotFoundException {
        synchronized (queue){
            try(RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getName()),"rw")){
                byte[] bufferSrc = new byte[(int) (message.getOffsetEnd()-message.getOffsetBeg())];
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.read(bufferSrc);
                Message diskMessage = (Message) BinaryTool.fromBytes(bufferSrc);
                diskMessage.setIsValid((byte) 0x0);
                byte[] bufferDest = BinaryTool.toBytes(diskMessage);
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.write(bufferDest);
            }
            Stat stat = readStat(queue.getName());
            if (stat.validCount > 0){
                stat.validCount -= 1;
            }
            writeStat(stat,queue.getName());
        }
    }

    public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
        LinkedList<Message> messages = new LinkedList<>();
        try(InputStream inputStream = new FileInputStream(queueName)){
            try(DataInputStream dataInputStream = new DataInputStream(inputStream)){
                long currentOffset = 0;
                while(true){
                    int messageSize = dataInputStream.readInt();
                    byte[] buffer = new byte[messageSize];
                    int actualSize = dataInputStream.read(buffer);
                    if (messageSize != actualSize){
                        throw new MqException("[MessageFileManager] 文件格式错误! queueName=" + queueName);
                    }
                    Message message = (Message) BinaryTool.fromBytes(buffer);
                    if (message.getIsValid() != 0x1){
                        currentOffset += (4+messageSize);
                        continue;
                    }
                    message.setOffsetBeg(currentOffset+4);
                    message.setOffsetEnd(currentOffset+4+messageSize);
                    currentOffset += (4+messageSize);
                    messages.add(message);
                }
            }catch (EOFException e){
                System.out.println("[MessageFileManager] 恢复 Message 数据完成!");
            }
        }
        return messages;
    }

    public boolean checkGC(String queueName){
        Stat stat = readStat(queueName);
        if (stat.totalCount > 2000 && (double) stat.validCount/(double) stat.totalCount < 0.5){
            return true;
        }
        return false;
    }

    public String getQueueDataNewPath(String queueName){
        return getQueueDir(queueName)+"/queue_data_new.txt";
    }

    public void gc(MSGQueue queue) throws MqException, IOException, ClassNotFoundException {
        // 进行 gc 的时候, 是针对消息数据文件进行大洗牌. 在这个过程中, 其他线程不能针对该队列的消息文件做任何修改.
        synchronized (queue) {
            // 由于 gc 操作可能比较耗时, 此处统计一下执行消耗的时间.
            long gcBeg = System.currentTimeMillis();

            // 1. 创建一个新的文件
            File queueDataNewFile = new File(getQueueDataNewPath(queue.getName()));
            if (queueDataNewFile.exists()) {
                // 正常情况下, 这个文件不应该存在. 如果存在, 就是意外~~ 说明上次 gc 了一半, 程序意外崩溃了.
                throw new MqException("[MessageFileManager] gc 时发现该队列的 queue_data_new 已经存在! queueName=" + queue.getName());
            }
            boolean ok = queueDataNewFile.createNewFile();
            if (!ok) {
                throw new MqException("[MessageFileManager] 创建文件失败! queueDataNewFile=" + queueDataNewFile.getAbsolutePath());
            }

            // 2. 从旧的文件中, 读取出所有的有效消息对象了. (这个逻辑直接调用上述方法即可, 不必重新写了)
            LinkedList<Message> messages = loadAllMessageFromQueue(queue.getName());

            // 3. 把有效消息, 写入到新的文件中.
            try (OutputStream outputStream = new FileOutputStream(queueDataNewFile)) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    for (Message message : messages) {
                        byte[] buffer = BinaryTool.toBytes(message);
                        // 先写四个字节消息的长度
                        dataOutputStream.writeInt(buffer.length);
                        dataOutputStream.write(buffer);
                    }
                }
            }

            // 4. 删除旧的数据文件, 并且把新的文件进行重命名
            File queueDataOldFile = new File(getQueueDataPath(queue.getName()));
            ok = queueDataOldFile.delete();
            if (!ok) {
                throw new MqException("[MessageFileManager] 删除旧的数据文件失败! queueDataOldFile=" + queueDataOldFile.getAbsolutePath());
            }
            // 把 queue_data_new.txt => queue_data.txt
            ok = queueDataNewFile.renameTo(queueDataOldFile);
            if (!ok) {
                throw new MqException("[MessageFileManager] 文件重命名失败! queueDataNewFile=" + queueDataNewFile.getAbsolutePath()
                        + ", queueDataOldFile=" + queueDataOldFile.getAbsolutePath());
            }

            // 5. 更新统计文件
            Stat stat = readStat(queue.getName());
            stat.totalCount = messages.size();
            stat.validCount = messages.size();
            writeStat(stat,queue.getName());

            long gcEnd = System.currentTimeMillis();
            System.out.println("[MessageFileManager] gc 执行完毕! queueName=" + queue.getName() + ", time="
                    + (gcEnd - gcBeg) + "ms");
        }
    }


}
